#include <Kharlia/Engine/Common.h>
#include <Kharlia/Engine/Level.h>
#include <Kharlia/Engine/TileData.h>
#include <Kharlia/Engine/Actor.h>
#include <Kharlia/Engine/Engine.h>
#include <Kharlia/Engine/World.h>
#include <Kharlia/Engine/Utils.h>
#include <Kharlia/Engine/Player.h>
#include <Kharlia/Engine/Net.h>
#include <Kharlia/Engine/Resource.h>
#include <Kharlia/Core/App.h>
#include <Kharlia/Core/ObjectUtil.h>
#include <Kharlia/Engine/Debug.h>

#define TILE_W 16
#define TILE_H 16
#define TILE_EX 2

#define TILE_W_F 16.0f
#define TILE_H_F 16.0f
#define TILE_EX_F 2.0f

#define TILE_VERTEX_COUNT 4
#define TILE_VERTEX_SIZE 2
#define TILE_TEX_COORD_COUNT 4
#define TILE_TEX_COORD_SIZE 2

#define ENABLE_ALLOC_DEBUG 0

namespace kharlia { namespace engine {

template <class T>
inline T Clamp(T const& value, T const& low, T const& high)
{
    return value < low ? low : (value > high ? high : value);
}

// -------------------------
// FTileLayer
// -------------------------

tuple FTileLayer::Pickler::getinitargs(FTileLayer& tl) {
    return py::make_tuple(tl.GetTileData());
}

FTileLayer::FTileLayer():
    FDrawable(),
    mTileData()
{
}

FTileLayer::FTileLayer(FSizeI const& size):
    FDrawable(),
    mTileData(new FTileData(size))
{
}

FTileLayer::FTileLayer(shared_ptr<FTileData> tile_data):
    FDrawable(),
    mTileData(tile_data)
{
}

FTileLayer::~FTileLayer() {
    
}

// -------------------------
// FTileGroup
// -------------------------

tuple FTileGroup::Pickler::getinitargs(FTileGroup& tg) {
    return make_tuple(tg.GetSize());
}

tuple FTileGroup::Pickler::getstate(FTileGroup& tg) {
    const int VERSION = 1;
    list layers;

    for (uint i = 0; i < tg.mLayers.size(); i++) {
        layers.append(tg.mLayers[i]);
    }

    return make_tuple(VERSION, layers);
}

void FTileGroup::Pickler::setstate(FTileGroup& tg, tuple state) {
    int version = extract<int>(state[0]);
    list layers = extract<list>(state[1]);

    for (int i = 0; i < len(layers); i++) {
        tg.AddLayer(i, extract<shared_ptr<FTileLayer>>(layers[i]));
    }
}

FTileGroup::FTileGroup(FSizeI const& size):
    mSize(size) {}

void FTileGroup::AddLayer(int index) {
    shared_ptr<FTileLayer> tl(new FTileLayer(mSize));
    AddLayer(index, tl);
}

void FTileGroup::AddLayer(int index, shared_ptr<FTileLayer> tl) {
    mLayers.insert(mLayers.begin() + index, tl);
}

shared_ptr<FTileLayer> const& FTileGroup::GetLayer(int index) {
    return mLayers.at(index);
}

// -------------------------
// FTileset
// -------------------------


FTileset::FTileset() {}
FTileset::FTileset(str name, uint16 size, uint16 offset):
    mName(name),
    mOffset(offset)
{
    SetSize(size);
}

void FTileset::SetSize(uint16 newsize) {
    mSize = newsize;
    mTileInfo.resize(PAGE_WIDTH * PAGE_HEIGHT * mSize, FTileInfo());
}

tuple FTileset::Pickler::getinitargs(FTileset& self) {
    return py::make_tuple(self.mName, self.mSize);
}

// We make pickling easier by treating the array of FTileInfo as
// a string, so check this.
static_assert(sizeof(char) == sizeof(uint8) == sizeof(FTileInfo),
              "pickling tile info as a string requires that each is one byte");

tuple FTileset::Pickler::getstate(FTileset& self) {
    const int VERSION = 2;
    // 2 - Add offset

    object tileInfo;
    if (self.mTileInfo.size() > 0) {
        // Save it as a string for efficiency
        uint8* front = (uint8*)&self.mTileInfo.front();
        tileInfo = str((char const*)front, self.mTileInfo.size());
    }

    tuple state = py::make_tuple(
        VERSION,
        self.mTexture,
        tileInfo,
        self.mOffset
        );
    return state;
}

void FTileset::Pickler::setstate(FTileset& self, tuple state) {
    int version = extract<int>(state[0]);
    self.mTexture = extract<shared_ptr<FTexture>>(state[1]);
    object tileInfo = state[2];
    if (!tileInfo.is_none()) {
        if (self.mTileInfo.size() != len(tileInfo))
            throw RuntimeError("Tileset size does not match size of tile info being unpickled");
        uint8* dest = (uint8*)&self.mTileInfo.front();
        char const* src = extract<char const*>(tileInfo);
        memcpy(dest, src, self.mTileInfo.size());
    }
    if (version >= 2)
        self.mOffset = extract<uint16>(state[3]);
}

// -------------------------
// CLevel
// -------------------------

/// InitArgs (handled in reduce): Name, GUID, Size
/// State: Layers, TileGroups, Actors, Collidables, Renderables
tuple CLevel::Pickler::getstate(CLevel& level) {
    // Versions:
    // 1 - Initial
    // 2 - Add tileset
    // 3 - Add mGameStarted
    // 4 - Change renderables from ARender to CActor
    // 5 - Multiple tilesets
    // 6 - ID and name with actors
    const int VERSION = 6;
    using namespace objects;

    list layerInfos, tileGroups, actors, collidables, renderables;

    CLevel::LayerList::iterator lit;
    for (lit = level.mLayers.begin(); lit != level.mLayers.end(); lit++) {
        layerInfos.append(*lit);
    }

    CLevel::TileGroupList::iterator tit;
    for (tit = level.mTileGroups.begin(); tit != level.mTileGroups.end(); tit++) {
        tileGroups.append(*tit);
    }

    object actor;
    for (uint i = 0; i < level.mActors.size(); i++) {
        actor = level.mActors[i]->GetSelf();
        if (!actor.attr("IsTransient")) {
            actors.append(actor);
            actors.append(level.mActors[i]->GetID());
            actors.append(level.mActors[i]->GetName());
            //actors.append(py::make_tuple(
            //    level.mActors[i]->GetActor(),
            //    level.mActors[i]->GetName(),
            //    actor
            //));
        }
    }

    for (uint i = 0; i < level.mCollidables.size(); i++) {
        collidables.append(level.mCollidables[i]);
    }

    for (uint i = 0; i < level.mRenderables.size(); i++) {
        renderables.append(level.mRenderables[i]);
    }

    object idict;
    //{
    //    instance<>* self = reinterpret_cast<instance<>*>(level.mSelf);
    //    if (self->dict)
    //        idict = object(handle<>(borrowed(self->dict)));
    //}

    return py::make_tuple(
        VERSION,
        idict, layerInfos, tileGroups, 
        actors, renderables, collidables,
        level.mIDCounter, level.mCollisionLayer, level.mGameStarted
        );
}

/// Restore the level from state. Actors are created and added to actor
/// list automatically as they're unpickled, this should probably be undone.
void CLevel::Pickler::setstate(CLevel& level, tuple state) {
    using namespace objects;

    assert(level.mLayers.size() == 0 && level.mTileGroups.size() == 0);

    int version      = extract<int>(state[0]);
    object idictO    = state[1];
    list layerInfos  = extract<list>(state[2]);
    list tileGroups  = extract<list>(state[3]);
    list actors      = extract<list>(state[4]);
    list renderables = extract<list>(state[5]);
    list collidables = extract<list>(state[6]);
    level.mIDCounter = extract<uint32>(state[7]);
    level.mCollisionLayer = extract<sint16>(state[8]);
    level.mGameStarted = extract<bool>(state[9]);

    if (!idictO.is_none()) {
        dict idict = extract<dict>(idictO);
        instance<>* self = reinterpret_cast<instance<>*>(level.mSelf);
        if (self->dict)
            PyDict_Update(self->dict, idict.ptr());
        else
            self->dict = incref(idict.ptr());
    }
    PY_FOR(obj, layerInfos) {
        shared_ptr<FLevelLayer> li = extract<shared_ptr<FLevelLayer>>(obj);
        level.mLayers.push_back(li);
    }
    PY_FOR(tg, tileGroups) {
        level.mTileGroups.push_back(extract<shared_ptr<FTileGroup>>(tg));
    }
    for (int i = 0; i < len(actors); i+= 3) {
        TPtr<CActor> actor = extract<TPtr<CActor>>(actors[i]);
        uint32 actorid = extract<uint32>(actors[i + 1]);
        object actorname = actors[i + 2];
        level.AddActor(actor, actorid, actorname);
    }
    PY_FOR(col, collidables) {
        level.mCollidables.push_back(extract<TPtr<ACollider>>(col));
    }
    PY_FOR(ren, renderables) {
        level.mRenderables.push_back(extract<TPtr<CActor>>(ren));
    }
}

object CLevel::msPlayerControllerClass;
object CLevel::msActorClass;

CLevel::CLevel(
    PyObject* self,
    object guid,
    unicode name,
    FSizeI const& size
):
    CObject(self),
    mSize(size),
    mName(name),
    mGUID(guid),
    mTileGroupSize(FTileGroup::WIDTH, FTileGroup::HEIGHT),
    mIDCounter(0),
    mGameStarted(false),
    mCollisionLayer(0)
{   
    if (ENABLE_ALLOC_DEBUG) {
        std::clog << "Level construct " << std::hex << (uint32)this << std::dec << "\n";
    }
    if (size.Width > MAX_WIDTH || size.Height > MAX_HEIGHT) {
        throw ValueError("level size limited to 255x255 tile groups");
    }
    if (mGUID.is_none())
        mGUID = CWorld::CreateGUID();

    mLogger = core::FLogger::GetLogger("engine");
    //mGUID = guid;
    //mName = name;
    //mSize = size;
    mNameHash = name.attr("__hash__")();
    assert(PyInt_CheckExact(mNameHash.ptr()));

    mTickChannel = handle<PyChannelObject>(PyChannel_New(NULL));
    //InitPhysics();
}

CLevel::~CLevel() {
    if (ENABLE_ALLOC_DEBUG) {
        std::clog << "Level destruct " << std::hex << (uint32)this << std::dec << "\n";
    }
    //ExitPhysics();
}

void CLevel::Finalize() {
    if (ENABLE_ALLOC_DEBUG) {
        std::clog << "Level finalize " << std::hex << (uint32)this << std::dec << "\n";
    }
    Clear();
    mTickChannel.reset();
}

void CLevel::ClearImpl() {
    if (mGameStarted)
        StopGame();
    
    ClearActors();

    mTileGroups.clear();
    mLayers.clear();
}

void CLevel::OnCreatedImpl() {}
void CLevel::OnDestroyedImpl() {}

void CLevel::Test(int num) {
    // Level test code
}

void CLevel::Tick(float deltaTime) {
    TickActors(deltaTime);
}

void CLevel::TickActors(float deltaTime) {
    for (uint i = 0; i < mActors.size(); i++) {
        mActors[i]->Tick(deltaTime);
    }
}

void CLevel::ClearActors() {
    for (uint i = mActors.size() - 1; mActors.size() > 0; i--) {
        TPtr<CActor> actor = mActors[i];
        DestroyActor(actor);
    }
    assert(len(mActorIDMap) == 0);
    assert(mRenderables.size() == 0);
    assert(mCollidables.size() == 0);
}

uint32 CLevel::CreateID() {
    if (mIDCounter >= 0xFFFFFFFF)
        throw NotImplementedError("no more IDs");
    // Zero is invalid
    return ++mIDCounter;
}

namespace {

// This function takes the argument names and produces a list of component
// types from it. names can be None, a string (type name), a type, or an iterable object
// that yields either types or type names.
list GetComponentTypes(object const& names) {
    object base = GetModuleAttr("AComponentBase");
    assert(PyType_Check(base.ptr()));
    list types;
    if (names.is_none()) {
        // do nothing
    }
    // Check if names is a string, and resolve it
    else if (PyString_Check(names.ptr())) {
        object comptype = FindGlobal(names);
        if (!PyType_Check(comptype.ptr()) ||
                !PyObject_IsSubclass(comptype.ptr(), base.ptr())) {
            throw TypeError("component name must resolve to subclass of AComponentBase");
        }
        types.append(comptype);
    }
    // Check if name is a type, and append it
    else if (PyType_Check(names.ptr()) &&
             PyObject_IsSubclass(names.ptr(), base.ptr())) {
        types.append(names);
    }
    // Attempt to iterate through the object
    else {
        py::handle<> iter(allow_null(PyObject_GetIter(names.ptr())));

        if (!iter) {
            PyErr_Clear();
            throw TypeError("comps must be None, string, or iterable object");
        }

        PY_FOR(compname, object(iter)) {
            // If its a string, resolve it to get the type
            if (PyString_Check(compname.ptr())) {
                object comptype = FindGlobal(compname);
                if (!PyType_Check(comptype.ptr()) ||
                        !PyObject_IsSubclass(comptype.ptr(), base.ptr())) {
                    throw TypeError("component name must resolve to subclass of AComponentBase");
                }
                types.append(comptype);
            }
            // If its a type, ensure its a component type
            else if (PyType_Check(compname.ptr()) &&
                     PyObject_IsSubclass(compname.ptr(), base.ptr())) {
                types.append(compname);
            }
            // Otherwise, something is wrong
            else {
                str os(compname);
                format msg("invalid component specification: %1%");
                msg % PyString_AS_STRING(os.ptr());
                throw TypeError(msg);
            }
        }
    }
    return types;
}

} // namespace

void CLevel::AddActor(TPtr<CActor> actor, uint32 id, object name) {
    if (id == 0)
        id = actor->GetID();
    if (name.is_none())
        name = actor->GetName();
    assert(id > 0);
    mActors.push_back(actor);
    if (!name.is_none())
        mActorMap[name] = actor->GetSelf();
    mActorIDMap[id] = actor->GetSelf();
}

void CLevel::RemoveActor(TPtr<CActor> actor, uint32 id) {
    if (id == 0)
        id = actor->GetID();
    mActors.erase(std::find(mActors.begin(), mActors.end(), actor));
    if (!actor->GetName().is_none())
        mActorMap[actor->GetName()].del();
    mActorIDMap[id].del();
}

object CLevel::CreateActorImpl(
    object compnames,
    TPtr<CActor> owner,
    object name,
    uint32 id)
{
    if (msActorClass.is_none())
        throw RuntimeError("actor class unspecified");
    if (!name.is_none() && !PyUnicode_Check(name.ptr()))
        throw ValueError("actor name must be None or unicode");

    list comptypes = GetComponentTypes(compnames);
    
    // Create ID
    if (id == 0) {
        id = CreateID();
        assert(id > 0);
    }

    // Check for ID collision
    if (mActorIDMap.contains(id)) {
        throw ValueError(format("id %1% already in use") % id);
    }
    // Check for name collision
    if (!name.is_none() && mActorMap.contains(name)) {
        throw ValueError(unicode(L"actor name already in use: ") + unicode(name));
    }
    // Construct and initialize
    object actorO = msActorClass();
    TPtr<CActor> actor = extract<TPtr<CActor>>(actorO);
    
    assert(actor->GetID() == 0);
    actor->Init(id, name, GetSelfPtr<CLevel>(), owner);
    assert(actor->GetID() == id);
    // Add to level's list and dict
    AddActor(actor);

    actor->SetRole(ENetRole::AUTHORITY);

    // Add components
    if (len(comptypes) > 0) {
        for (int i = 0; i < len(comptypes); i++) {
            object compio = comptypes[i]();
            TPtr<AComponent> compi = extract<TPtr<AComponent>>(compio);
            actor->AddComponent(compi);
        }
    }

    GetLoggerPtr()->Debug(str("created actor: ") + str(actor));

    return actorO;
}

void CLevel::DestroyActorImpl(TPtr<CActor> actor) {
    // This level can only destroy its own actors
    if (actor->GetLevel().GetPtr() != this) {
        throw RuntimeError("actor is not in this level");
    }

    GetLoggerPtr()->Debug(str("destroying actor: ") + str(actor));

    uint32 id = actor->GetID();
    assert(id != 0);

    actor->Exit();
    assert(actor->GetID() == 0);

    RemoveActor(actor, id);
}

bool CLevel::RenameActor(TPtr<CActor> const& actor, object const& newname) {
    if (actor->GetLevel().GetPtr() != this)
        return false;

    object current = actor->GetName();
    if (!current.is_none())
        mActorMap[current].del();

    if (newname.is_none()) {
        actor->SetName(newname);
        return true;
    }

    if (mActorMap.contains(newname))
        return false; // name conflict

    actor->SetName(newname);
    mActorMap[newname] = actor;
    return true;
}

TPtr<CActor> CLevel::GetActorByTag(object tag) const {
    extract<unicode> extractor(tag);
    if (!extractor.check())
        throw TypeError("not a unicode string");
    for (uint i = 0; i < mActors.size(); i++) {
        if (mActors[i]->GetTag() == tag) {
            return mActors[i];
        }
    }
    return TPtr<CActor>();
}

object CLevel::GetActorsByTagPy(object tag) const {
    if (tag.is_none())
        throw ValueError("tag is None");
    list actors;
    for (uint i = 0; i < mActors.size(); i++) {
        if (mActors[i]->GetTag() == tag) {
            actors.append(mActors[i]);
        }
    }
    return actors;
}

TPtr<CActor> CLevel::GetActorByID(uint32 id) const {
    return extract<TPtr<CActor>>(mActorIDMap.get(id, object()));
}

TPtr<CActor> CLevel::GetActorByName(object name) const {
    if (name.is_none())
        throw ValueError("name is None");
    return extract<TPtr<CActor>>(mActorMap.get(name, object()));
}

shared_ptr<FLevelLayer> CLevel::CreateLayer(int index) {
    if (mTileGroups.size() < 1) {
        for (int i = 0; i < mSize.Height * mSize.Width; i++) {
            shared_ptr<FTileGroup> tileGroup = make_shared<FTileGroup>(mTileGroupSize);
            mTileGroups.push_back(tileGroup);
        }
    }
    for (uint i = 0; i < mTileGroups.size(); i++) {
        mTileGroups[i]->AddLayer(index);
    }
    shared_ptr<FLevelLayer> info = make_shared<FLevelLayer>();
    info->Index = index;
    mLayers.insert(mLayers.begin() + index, info);
    return info;
}

void CLevel::AddedToWorld(TPtr<CWorld> world) {
    if (mWorld.Lock())
        throw RuntimeError("Level is already in a world");
    mWorld = world;
}

void CLevel::RemovedFromWorld(TPtr<CWorld> world) {
    if (mWorld.Lock() != world)
        throw RuntimeError("Level is not in the world");
    mWorld.Reset();
}

TPtr<CGame> CLevel::GetGame() const {
    TPtr<CWorld> world = mWorld.Lock();
    return world ? world->GetGame() : TPtr<CGame>();
}

void CLevel::StartGame() {
    if (mGameStarted)
        return;

    for (uint i = 0; i < mActors.size(); i++) {
        std::vector<FComponentEntry> const& comps = mActors[i]->GetComponents();
        for (uint e = 0; e < comps.size(); e++) {
            comps[e].Component->OnGameStarted();
        }
    }

    mGameStarted = true;
}

void CLevel::StopGame() {
    if (!mGameStarted)
        return;

    for (uint i = 0; i < mActors.size(); i++) {
        std::vector<FComponentEntry> const& comps = mActors[i]->GetComponents();
        for (uint e = 0; e < comps.size(); e++) {
            comps[e].Component->OnGameStopped();
        }
    }

    mGameStarted = false;
}

void CLevel::SetTiles(int layer, tile_t tile) {
    std::vector<shared_ptr<FTileGroup>>::iterator it;
    for (it = mTileGroups.begin(); it != mTileGroups.end(); it++) {
        (*it)->GetLayer(layer)->GetTileData()->SetTiles(tile);
    }
}

void CLevel::SetTiles(int layer, FVector2I const& topos, FSizeI const& size, tile_t const* tiles) {
    //tile_t const* front = &tiledata->GetTiles().front();
    shared_ptr<FTileData> td = make_shared<FTileData>(size);
    td->SetTiles(topos, size, tiles);
    //SetTiles(layer, topos, tiledata->GetSize(), front);
}


void CLevel::SetTiles(int layerIndex, FVector2I const& toPos, shared_ptr<FTileData> tileData) {
    int const& tileDataWidth = tileData->GetSize().Width;
    int const& tileDataHeight = tileData->GetSize().Height;

    // Top left and bottom right tile group positions
    FVector2I topLeftPos(toPos.X / mTileGroupSize.Width,
                         toPos.Y / mTileGroupSize.Height);
    FVector2I bottomRightPos((toPos.X + (tileDataWidth - 1)) / mTileGroupSize.Width,
                             (toPos.Y + (tileDataHeight - 1)) / mTileGroupSize.Height);
    
    // tile group width, height, and count
    int destTileGroupsWidth = bottomRightPos.X - topLeftPos.X + 1;
    int destTileGroupsHeight = bottomRightPos.Y - topLeftPos.Y + 1;
    int destTileGroupsCount = destTileGroupsWidth * destTileGroupsHeight;

    // Piece width, height; current tilegroup x, y; total offset x, y; tds index
    int pieceWidth, pieceHeight, groupX, groupY, offsetX, offsetY;
    shared_ptr<FTileLayer> layer;

    // get our position within the tile group
    // start in top left corner
    groupX = toPos.X % mTileGroupSize.Width;
    groupY = toPos.Y % mTileGroupSize.Height;
    offsetY = 0;
    for (int Y = 0; Y < destTileGroupsHeight; Y++) {
        // Calculate how many tiles from the brush can we fit in this group
        pieceHeight = mTileGroupSize.Height - groupY;
        if (pieceHeight > (tileDataHeight - offsetY))
            pieceHeight = tileDataHeight - offsetY;
        offsetX = 0;
        for (int X = 0; X < destTileGroupsWidth; X++) {
            pieceWidth = mTileGroupSize.Width - groupX;
            if (pieceWidth > (tileDataWidth - offsetX))
                pieceWidth = tileDataWidth - offsetX;
            
            // Destination tile group location on the level
            FVector2I destTileGroupPos(topLeftPos.X + X, topLeftPos.Y + Y);
            if (destTileGroupPos.X < 0 || destTileGroupPos.X > mSize.Width ||
                destTileGroupPos.Y < 0 || destTileGroupPos.Y > mSize.Height)
                continue;

            layer = this->GetTileGroup(destTileGroupPos)->GetLayer(layerIndex);
            layer->GetTileData()->CopyTiles(FVector2I(groupX, groupY),
                                            FRectI(offsetX, offsetY, pieceWidth, pieceHeight),
                                            tileData);
            // Must signal that tiles have been changed to recreate
            // texture coordinate buffers
            layer->SetModified(true);

            offsetX += pieceWidth;
            groupX = (toPos.X + offsetX) % mTileGroupSize.Width;
        }
        offsetY += pieceHeight;
        groupY = (toPos.Y + offsetY) % mTileGroupSize.Height;
    }
}
void CLevel::SetTiles(int layer, FVector2I const& topos, FSizeI const& size, tile_t tile) {
    // top left position of tile group on the level
    FVector2I tlp(topos.X / mTileGroupSize.Width,
                 topos.Y / mTileGroupSize.Height);

    // bottom right position of tile group on the level
    FVector2I brp((topos.X + size.Width) / mTileGroupSize.Width,
                 (topos.Y + size.Height) / mTileGroupSize.Height);

    // width and height of affected tile groups
    int tgw = brp.X - tlp.X + 1;
    int tgh = brp.Y - tlp.Y + 1;

    for (int h = 0; h < tgh; h++) {
        for (int w = 0; w < tgw; w++) {
            FVector2I tgp(tlp.X + w, tlp.Y + h);
            if (tgp.X < 0 || tgp.X >= mSize.Width ||
                tgp.Y < 0 || tgp.Y >= mSize.Height) {
                    continue;
            }
            // adjust from tile coordinates to pos on tiledata
            FVector2I adjpos(topos.X - (mTileGroupSize.Width * w),
                            topos.Y - (mTileGroupSize.Height * h));
            GetTileGroup(tgp)->
                GetLayer(layer)->
                GetTileData()->
                SetTiles(adjpos, size, tile);
        }
    }
}

FTileInfo const* CLevel::GetTileInfo(int layerindex, FVector2I const& pos) const {
    if (mLayers.size() == 0)
        return nullptr;
    shared_ptr<FLevelLayer> layer = GetLayer(layerindex);
    if (!layer)
        return nullptr;
    shared_ptr<FTileGroup> const& tg = GetTileGroupAt(pos);
    shared_ptr<FTileLayer> const& tl = tg->GetLayer(layerindex);
    uint16 idx = (mTileGroupSize.Width * (pos.Y % FTileGroup::WIDTH)) +
                    (pos.X % FTileGroup::HEIGHT);
    tile_t code = tl->GetTileData()->GetTiles().at(idx);
    return &layer->Tileset->GetTileInfoByIndex(code);
}

void CLevel::AddCollidable(TPtr<ACollider> col) {
    if (col->GetLevel().GetPtr() != this) {
        throw RuntimeError("component is not owned by this level");
    }
    if (std::find(mCollidables.begin(), mCollidables.end(), col) != mCollidables.end())
        return;
    mCollidables.push_back(col);
}

void CLevel::RemoveCollidable(TPtr<ACollider> col) {
    if (col->GetLevel().GetPtr() != this) {
        throw RuntimeError("component is not owned by this level");
    }
    for (uint i = 0; i < mCollidables.size(); i++) {
        if (mCollidables[i] == col) {
            mCollidables.erase(mCollidables.begin() + i);
            break;
        }
    }
}

void CLevel::BuildTileCollisionPolygons(shared_ptr<FTileLayer> layer, std::vector<FRectI>& out) {
    //out.clear();

    //if (!layer)
    //    throw ValueError("layer is None");
    //if (!layer->GetTileData())
    //    throw ValueError("empty layer");
    //if (mTilesets.size() == 0)
    //    throw RuntimeError("attempted to build tile collision polygons without defined tileset");

    //FSizeI const& size = layer->GetTileData()->GetSize();
    //std::vector<tile_t> const& tiles = layer->GetTileData()->GetTiles();
    //std::vector<FTileInfo> const& tileInfo = mTileset->GetTileInfo();
    //FRectI current;
    //bool reset = true;

    //for (int h = 0, i = 0; h < size.Height; h++) {
    //    for (int w = 0; w < size.Width; w++, i++) {
    //        // Create new rectangle for current position
    //        if (reset) {
    //            current.Pos.X = w;
    //            current.Pos.Y = h;
    //            current.Size.Width = 0;
    //            current.Size.Height = 1;
    //            reset = false;
    //        }
    //        // If tile blocks, increase width of current rectangle
    //        if (tileInfo.at(tiles[i]).Blocking) {
    //            current.Size.Width += 1;
    //        }
    //        // If tile doesn't block and nothing in the current rectangle, move it over
    //        else if (current.Size.Width == 0) {
    //            current.Pos.X += 1;
    //        }
    //        // Otherwise, we have a complete rectangle, add it to the list
    //        else {
    //            out.push_back(current);
    //            reset = true;
    //        }
    //    }
    //    if (current.Size.Width > 0)
    //        out.push_back(current);
    //    reset = true;
    //}
}

object CLevel::BuildTileCollisionPolygonsPy(shared_ptr<FTileLayer> layer) {
    std::vector<FRectI> polys;

    BuildTileCollisionPolygons(layer, polys);

    if (polys.size() == 0)
        return object();
    
    list out;
    for (uint i = 0; i < polys.size(); i++) {
        out.append(polys[i]);
    }
    return out;
}

bool CLevel::AddRenderable(TPtr<CActor> r) {
    if (std::find(mRenderables.begin(), mRenderables.end(), r) != mRenderables.end())
        return true;
    mRenderables.push_back(r);
    return true;
}

bool CLevel::RemoveRenderable(TPtr<CActor> r) {
    auto it = std::find(mRenderables.begin(), mRenderables.end(), r);
    if (it == mRenderables.end())
        return false;
    mRenderables.erase(it);
    return true;
}

bool CLevel::CollideTiles(FRectF const& dest, int layer) {
    FVector2I tilePos;
    FTileInfo const* tileInfo = nullptr;

    // Check all tiles within the destination rect
    int w = int(ceil(dest.GetRight()) - floor(dest.GetLeft()));
    int h = int(ceil(dest.GetBottom()) - floor(dest.GetTop()));
    try {
        for (int hi = 0; hi < h; hi++) {
            for (int wi = 0; wi < w; wi++) {
                tilePos.X = int(dest.Pos.X) + wi;
                tilePos.Y = int(dest.Pos.Y) + hi;
                // TODO: Optimize locating the tile layer rather than
                //       looking through the layer map each time.
                tileInfo = GetTileInfo(layer, tilePos);
                assert(tileInfo);
                if (tileInfo->Blocking) {
                    return true;
                }
            }
        }
    }
    catch (std::out_of_range&) {
        object msg("collision tile pos invalid: L={} X={} Y={}");
        msg = msg.attr("format")(layer, tilePos.X, tilePos.Y);
        mLogger->Warning(msg);
    }
    return false;
}

bool CLevel::MoveActor(
    TPtr<CActor> const& actor,
    FVector3F const& delta,
    bool smooth
    )
{
    const double TOUCH_TIME_LIMIT = 1.0;
    const float BOUNDING_BOX_ADJUST = 1.0f/32.0f;

    assert(actor->GetLevel().GetPtr() == this);

    TPtr<ATransform> const& trans = actor->GetTransform();
    if (!trans)
        return false;

    TPtr<ACollider> const& col = actor->GetCollider();
    if (!col) {
        trans->AddPosition(delta);
        return true;
    }

    FVector3F const& pos = trans->GetPosition();
    FRectF const& bounds = col->GetBounds();
    FVector3F dest       = pos + delta;
    int collLayer        = col->GetCollisionLayer();
    if (collLayer == -1)
        collLayer = mCollisionLayer;

    FRectF destRect = bounds;
    destRect.Pos.X += dest.X;
    destRect.Pos.Y += dest.Y;

    // Adjust
    destRect.Pos.X += BOUNDING_BOX_ADJUST;
    destRect.Pos.Y += BOUNDING_BOX_ADJUST;
    destRect.Size.Width -= BOUNDING_BOX_ADJUST;
    destRect.Size.Height -= BOUNDING_BOX_ADJUST;

    // Check if we're in the level bounds
    if (destRect.GetLeft() < 0.0f ||
        destRect.GetTop() < 0.0f ||
        destRect.GetRight() > (mSize.Width * FTileGroup::WIDTH) ||
        destRect.GetBottom() > (mSize.Height * FTileGroup::HEIGHT))
    {
        return false;
    }

    // If actor bounds has no area, stop here.
    if (bounds.Size.GetArea() <= 0.0f) {
        return false;
    }

    // Check tile collisions if the level has tiles and a tileset to define
    // which tiles are blocking
    if (col->GetCollideTiles() && mTileGroups.size() > 0 && mLayers.size() > 0) {
        if (CollideTiles(destRect, collLayer)) {
            // TODO: HitWall() notification
            return false;
        }
    }
    // Check actor collisions
    if (col->GetCollideActors()) {
        //FTileGroupNumber destTGN = ATransform::CalculateTileGroupNumber(dest);
        for (uint i = 0; i < mActors.size(); i++) {
            TPtr<ACollider> const& targetCol = mActors[i]->GetCollider();
            if (!targetCol)
                continue;
            TPtr<ATransform> const& targetTrans = targetCol->GetActor()->GetTransform();
            int targetColLayer = targetCol->GetCollisionLayer();
            if (targetColLayer == -1)
                targetColLayer = mCollisionLayer;

            // Simple checks
            if (/*targetTrans->GetTileGroupNumber() != destTGN ||*/
                !targetCol->GetCollideActors() ||
                targetColLayer != collLayer ||
                targetCol->GetActor() == actor)
                continue;

            // Check for valid bounds
            FRectF const& targetBounds = targetCol->GetBounds();
            if (targetBounds.Size.GetArea() <= 0.0f)
                continue;

            // Get target rect
            FRectF targetRect = targetBounds;
            targetRect.Pos.X += targetTrans->GetPosition().X;
            targetRect.Pos.Y += targetTrans->GetPosition().Y;

            // Bounding box check
            if ((targetRect.GetRight() < destRect.GetLeft()) ||
                (targetRect.GetLeft() > destRect.GetRight()) ||
                (targetRect.GetBottom() < destRect.GetTop()) ||
                (targetRect.GetTop() > destRect.GetBottom())) {
                continue;
            }
            // Bump check
            else if (col->GetBlockActors() && targetCol->GetBlockActors()) {
                col->Bump(targetCol);
                targetCol->Bump(col);
                return false;
            }
            // We're touching but not blocking
            else if (!col->IsTouching(targetCol)) {
                col->BeginTouch(targetCol);
                targetCol->BeginTouch(col);
            }
        }
    }

    trans->SetPosition(dest);

    // Handle touch endings
    std::vector<TWeakPtr<ACollider>> const& touchers = col->GetTouchers();
    for (uint i = 0; i < touchers.size();) {
        TPtr<ACollider> toucher = touchers[i].Lock();
        assert(toucher);

        // Check if bounding boxes are overlapping still
        TPtr<ATransform> trans = toucher->GetActor()->GetTransform();
        if (!trans) {
            col->EndTouch(toucher);
            toucher->EndTouch(col);
            continue;
        }

        FRectF box = toucher->GetBounds();
        box.Pos.X += trans->GetPosition().X;
        box.Pos.Y += trans->GetPosition().Y;
        if (box.GetRight() < destRect.GetLeft() ||
                box.GetLeft() > destRect.GetRight() ||
                box.GetBottom() < destRect.GetTop() ||
                box.GetTop() > destRect.GetBottom()) {
            col->EndTouch(toucher);
            toucher->EndTouch(col);
            continue;
        }
        
        i++;
    }

    return true;
}

void CLevel::SetPlayerControllerClass(object cls) {
    object pccls = GetTypeObject(type_id<APlayerController>());
    assert(!pccls.is_none());
    if (!PyObject_IsSubclass(cls.ptr(), pccls.ptr()))
        throw TypeError("player controller class must be subclass of APlayerControllerBase");
    msPlayerControllerClass = cls;
}

void CLevel::SetActorClass(object cls) {
    if (!cls.is_none()) {
        object actorBaseClass = GetTypeObject(type_id<CActor>());
        if (!PyObject_IsSubclass(cls.ptr(), actorBaseClass.ptr()))
            throw TypeError("actor class must be subclass of CActorBase");
    }
    msActorClass = cls;
}

tuple FLevelLayer::Pickler::getinitargs(FLevelLayer& self) {
    return make_tuple();
}

object FLevelLayer::Pickler::getstate(FLevelLayer& li) {
    const int VERSION = 1;
    rn::BitStream out;
    out.Write(li.Index);
    out.Write(li.Red);
    out.Write(li.Green);
    out.Write(li.Blue);
    out.Write(li.Alpha);
    out.Write(li.NoCollision);
    out.Write(li.Visible);
    return py::make_tuple(
        VERSION,
        str((char const*)out.GetData(), out.GetNumberOfBytesUsed()),
        li.Tileset,
        li.Name
        );
}

void FLevelLayer::Pickler::setstate(FLevelLayer& li, object stateb) {
    int version = extract<int>(stateb[0]);
    {
        str state = extract<str>(stateb[1]);
        rn::BitStream in((unsigned char*)PyString_AS_STRING(state.ptr()),
                            PyString_GET_SIZE(state.ptr()), false);
        in.Read(li.Index);
        in.Read(li.Red);
        in.Read(li.Green);
        in.Read(li.Blue);
        in.Read(li.Alpha);
        in.Read(li.NoCollision);
        in.Read(li.Visible);
    }
    li.Tileset = extract<shared_ptr<FTileset>>(stateb[2]);
    li.Name = extract<unicode>(stateb[3]);
}

KH_BEGIN_GC(CLevel)
    KH_GC_TPTR(thisx->mWorld)
    BOOST_FOREACH(TPtr<CActor>& a, thisx->mActors) {
        KH_GC_TPTR(a)
    }
KH_END_GC()

void _InitLevelClasses() {
    _InitLevelBindings();
}

void _ExitLevelClasses() {
    CLevel::msPlayerControllerClass = object();
    CLevel::msActorClass = object();
}

}} //namespace Kharlia
